package com.lorentzos.flingswipe;

import ohos.agp.animation.Animator;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.utils.Point;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;

/**
 * Created by dionysis_lorentzos on 5/8/14
 * for package com.lorentzos.swipecards
 * and project Swipe cards.
 * Use with caution dinausaurs might appear!
 */
public class FlingCardListener implements Component.TouchEventListener {
    private static final int INVALID_POINTER_ID = -1;

    private final float mObjectX;
    private final float mObjectY;
    private final int mObjectH;
    private final int mObjectW;
    private final int mParentWidth;
    private final FlingListener mFlingListener;
    private final Object mDataObject;
    private final float mHalfWidth;
    private float BASE_ROTATION_DEGREES;

    private float mPosX;
    private float mPosY;
    private float mDownTouchX;
    private float mDownTouchY;

    // The active pointer is the one currently moving our object.
    private int mActivePointerId = INVALID_POINTER_ID;
    private Component mFrame = null;

    private final int TOUCH_ABOVE = 0;
    private final int TOUCH_BELOW = 1;
    private int mTouchPosition;
    private final Object obj = new Object();
    private boolean mIsAnimationRunning = false;
    private float MAX_COS = (float) Math.cos(Math.toRadians(45));

    public FlingCardListener(
            ComponentContainer parent, Component frame, Object itemAtPosition, FlingListener flingListener) {
        this(parent, frame, itemAtPosition, 15f, flingListener);
    }

    public FlingCardListener(
            ComponentContainer parent,
            Component frame,
            Object itemAtPosition,
            float rotation_degrees,
            FlingListener flingListener) {
        super();
        this.mFrame = frame;
        this.mObjectX = frame.getContentPositionX();
        this.mObjectY = frame.getContentPositionY();
        this.mObjectH = frame.getHeight();
        this.mObjectW = frame.getWidth();
        this.mHalfWidth = mObjectW / 2f;
        this.mDataObject = itemAtPosition;
        this.mParentWidth = parent.getWidth();
        this.BASE_ROTATION_DEGREES = rotation_degrees;
        this.mFlingListener = flingListener;
    }

    @Override
    public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
        MmiPoint point = touchEvent.getPointerScreenPosition(0);
        switch (touchEvent.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                float downTouchX = point.getX();
                float downTouchY = point.getY();
                mDownTouchX = downTouchX;
                mDownTouchY = downTouchY;

                // to prevent an initial jump of the magnifier, aposX and aPosY must
                // have the values from the magnifier frame
                if (mPosX == 0) {
                    mPosX = mFrame.getContentPositionX();
                }
                if (mPosY == 0) {
                    mPosY = mFrame.getContentPositionY();
                }

                if (downTouchY - mFrame.getContentPositionY() < mObjectH / 2f) {
                    mTouchPosition = TOUCH_ABOVE;
                } else {
                    mTouchPosition = TOUCH_BELOW;
                }
                break;
            case TouchEvent.PRIMARY_POINT_UP:
                mActivePointerId = INVALID_POINTER_ID;
                resetCardViewOnStack();
                break;
            case TouchEvent.OTHER_POINT_DOWN:
                break;
            case TouchEvent.OTHER_POINT_UP:
                break;
            case TouchEvent.POINT_MOVE:
                // Find the index of the active pointer and fetch its position
                final float moveTouchX = point.getX();
                final float moveTouchY = point.getY();

                // Calculate the distance moved
                final float dx = moveTouchX - mDownTouchX;
                final float dy = moveTouchY - mDownTouchY;

                // Move the frame
                mPosX += dx;
                mPosY += dy;

                // calculate the rotation degrees
                float distobjectX = mPosX - mObjectX;

                float rotation = BASE_ROTATION_DEGREES * 2.f * distobjectX / mParentWidth;
                if (mTouchPosition == TOUCH_BELOW) {
                    rotation = -rotation;
                }

                // move the component
                mFrame.setTranslationX(mFrame.getContentPositionX() + dx);
                mFrame.setTranslationY(mFrame.getContentPositionY() + dy);
                mFrame.setRotation(rotation);
                mFlingListener.onScroll(getScrollProgressPercent());

                // save the touch point position
                mDownTouchX = moveTouchX;
                mDownTouchY = moveTouchY;
                break;
            case TouchEvent.CANCEL:
                mActivePointerId = INVALID_POINTER_ID;
                break;
        }
        return true;
    }

    private float getScrollProgressPercent() {
        if (movedBeyondLeftBorder()) {
            return -1f;
        } else if (movedBeyondRightBorder()) {
            return 1f;
        } else {
            float zeroToOneValue = (mPosX + mHalfWidth - leftBorder()) / (rightBorder() - leftBorder());
            return zeroToOneValue * 2f - 1f;
        }
    }

    private boolean resetCardViewOnStack() {
        if (movedBeyondLeftBorder()) {
            // Left Swipe
            onSelected(true, getExitPoint(-mObjectW), 100);
            mFlingListener.onScroll(-1.0f);
        } else if (movedBeyondRightBorder()) {
            // Right Swipe
            onSelected(false, getExitPoint(mParentWidth), 100);
            mFlingListener.onScroll(1.0f);
        } else {
            float abslMoveDistance = Math.abs(mPosX - mObjectX);
            mPosX = 0;
            mPosY = 0;
            mDownTouchX = 0;
            mDownTouchY = 0;
            mFrame.createAnimatorProperty()
                    .setDuration(200)
                    .setCurveType(Animator.CurveType.OVERSHOOT)
                    .moveToX(mObjectX)
                    .moveToY(mObjectY)
                    .rotate(0)
                    .start();
            mFlingListener.onScroll(0.0f);
            if (abslMoveDistance < 4.0) {
                mFlingListener.onClick(mDataObject);
            }
        }
        return false;
    }

    private boolean movedBeyondLeftBorder() {
        return mPosX + mHalfWidth < leftBorder();
    }

    private boolean movedBeyondRightBorder() {
        return mPosX + mHalfWidth > rightBorder();
    }

    public float leftBorder() {
        return mParentWidth / 4.f;
    }

    public float rightBorder() {
        return 3 * mParentWidth / 4.f;
    }

    public void onSelected(final boolean isLeft, float exitY, long duration) {
        mIsAnimationRunning = true;
        float exitX;
        if (isLeft) {
            exitX = -mObjectW - getRotationWidthOffset();
        } else {
            exitX = mParentWidth + getRotationWidthOffset();
        }

        this.mFrame
                .createAnimatorProperty()
                .setDuration(duration)
                .setCurveType(Animator.CurveType.ACCELERATE)
                .moveByX(exitX)
                .moveByY(exitY)
                .setStateChangedListener(
                        new Animator.StateChangedListener() {
                            @Override
                            public void onStart(Animator animator) {}

                            @Override
                            public void onStop(Animator animator) {}

                            @Override
                            public void onCancel(Animator animator) {}

                            @Override
                            public void onEnd(Animator animator) {
                                if (isLeft) {
                                    mFlingListener.onCardExited();
                                    mFlingListener.leftExit(mDataObject);
                                } else {
                                    mFlingListener.onCardExited();
                                    mFlingListener.rightExit(mDataObject);
                                }
                                mIsAnimationRunning = false;
                            }

                            @Override
                            public void onPause(Animator animator) {}

                            @Override
                            public void onResume(Animator animator) {}
                        })
                .rotate(getExitRotation(isLeft))
                .start();
    }

    /**
     * Starts a default left exit animation.
     */
    public void selectLeft() {
        if (!mIsAnimationRunning) {
            onSelected(true, mObjectY, 200);
        }
    }

    /**
     * Starts a default right exit animation.
     */
    public void selectRight() {
        if (!mIsAnimationRunning) {
            onSelected(false, mObjectY, 200);
        }
    }

    private float getExitPoint(int exitXPoint) {
        float[] x = new float[2];
        x[0] = mObjectX;
        x[1] = mPosX;

        float[] y = new float[2];
        y[0] = mObjectY;
        y[1] = mPosY;

        LinearRegression regression = new LinearRegression(x, y);

        // Your typical y = ax+b linear regression
        return (float) regression.slope() * exitXPoint + (float) regression.intercept();
    }

    private float getExitRotation(boolean isLeft) {
        float rotation = BASE_ROTATION_DEGREES * 2.f * (mParentWidth - mObjectX) / mParentWidth;
        if (mTouchPosition == TOUCH_BELOW) {
            rotation = -rotation;
        }
        if (isLeft) {
            rotation = -rotation;
        }
        return rotation;
    }

    /**
     * When the object rotates it's width becomes bigger.
     * The maximum width is at 45 degrees.
     * <p/>
     * The below method calculates the width offset of the rotation.
     *
     * @return rotation width offset
     */
    private float getRotationWidthOffset() {
        return mObjectW / MAX_COS - mObjectW;
    }

    public void setRotationDegrees(float degrees) {
        this.BASE_ROTATION_DEGREES = degrees;
    }

    public boolean isTouching() {
        return this.mActivePointerId != INVALID_POINTER_ID;
    }

    public Point getLastPoint() {
        return new Point(this.mPosX, this.mPosY);
    }

    protected interface FlingListener {
        void onCardExited();

        void leftExit(Object dataObject);

        void rightExit(Object dataObject);

        void onClick(Object dataObject);

        void onScroll(float scrollProgressPercent);
    }
}
